
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: Schema.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.dellroad.sidekar.schema.SchemaModificationException;
import org.dellroad.sidekar.schema.change.AddEntityChange;
import org.dellroad.sidekar.schema.change.AddFieldChange;
import org.dellroad.sidekar.schema.change.AddIndexChange;
import org.dellroad.sidekar.schema.change.EntityAbstractnessChange;
import org.dellroad.sidekar.schema.change.RemoveEntityChange;
import org.dellroad.sidekar.schema.change.RemoveFieldChange;
import org.dellroad.sidekar.schema.change.RemoveIndexChange;
import org.dellroad.sidekar.schema.change.RenameEntityChange;
import org.dellroad.sidekar.schema.change.SchemaChangeSwitch;
import org.dellroad.sidekar.schema.model.ArrayFieldModel;
import org.dellroad.sidekar.schema.model.AtomicFieldModel;
import org.dellroad.sidekar.schema.model.BigIntegerFieldModel;
import org.dellroad.sidekar.schema.model.BooleanFieldModel;
import org.dellroad.sidekar.schema.model.ByteFieldModel;
import org.dellroad.sidekar.schema.model.CharacterFieldModel;
import org.dellroad.sidekar.schema.model.DateFieldModel;
import org.dellroad.sidekar.schema.model.DoubleFieldModel;
import org.dellroad.sidekar.schema.model.FieldModel;
import org.dellroad.sidekar.schema.model.FieldModelSwitch;
import org.dellroad.sidekar.schema.model.FloatFieldModel;
import org.dellroad.sidekar.schema.model.IndexModel;
import org.dellroad.sidekar.schema.model.IntegerFieldModel;
import org.dellroad.sidekar.schema.model.ListFieldModel;
import org.dellroad.sidekar.schema.model.LongFieldModel;
import org.dellroad.sidekar.schema.model.MapFieldModel;
import org.dellroad.sidekar.schema.model.ReferenceFieldModel;
import org.dellroad.sidekar.schema.model.SchemaModel;
import org.dellroad.sidekar.schema.model.SetFieldModel;
import org.dellroad.sidekar.schema.model.ShortFieldModel;
import org.dellroad.sidekar.schema.model.SimpleFieldModel;
import org.dellroad.sidekar.schema.model.StringFieldModel;
import org.dellroad.sidekar.util.SidekarException;
import org.dellroad.stuff.graph.TopologicalSorter;

/**
 * Class that contains the data structures that represent a live {@link Database} schema.
 * This is essentially just the database's {@link Entity}s and {@link Field}s. This class is the starting point
 * for "Sidekar reflection".
 *
 * <p>
 * This class also provides methods used to modify the schema. Schema modifications may be performed
 * at any time but while updates are performed all other access to the database will be blocked.
 * </p>
 *
 * <p>
 * Instances are only functional while the associated {@link Database} remains open.
 * </p>
 */
public class Schema extends SchemaObject implements SchemaChangeSwitch {

    private final HashMap<String, Entity<?>> nameMap = new HashMap<String, Entity<?>>();
    private final HashMap<Class<?>, Entity<?>> classMap = new HashMap<Class<?>, Entity<?>>();

    // Implicit fields
    private final IdField idField = new IdField();
    private final AttachedField attachedField = new AttachedField();
    private final EntityField entityField = new EntityField(this);

// Constructor

    Schema(Database db, SchemaModel schemaModel) {
        super(db);

        // Compile schema
        SchemaModelCompiler compiler = new SchemaModelCompiler(this, schemaModel);
        compiler.compile();
        this.nameMap.putAll(compiler.getNameMap());
        this.classMap.putAll(compiler.getClassMap());

        // Update state from schema
        this.recomputeDerivedInfo();
    }

// Public methods

    /**
     * Get an entity by name.
     *
     * @param name the name of the desired entity
     * @throws IllegalArgumentException if the named entity does not exist
     */
    public synchronized Entity<?> getEntity(String name) {
        Entity<?> entity = this.nameMap.get(name);
        if (entity == null)
            throw new IllegalArgumentException("no entity named \"" + name + "\" is defined in this database");
        return entity;
    }

    /**
     * Get an entity by model class.
     *
     * @param modelClass user database model class
     * @throws IllegalArgumentException if the model class is not associated with any entity in this database
     */
    @SuppressWarnings("unchecked")
    public synchronized <T> Entity<T> getEntity(Class<T> modelClass) {
        Entity<T> entity = (Entity<T>)this.classMap.get(modelClass);
        if (entity == null)
            throw new IllegalArgumentException("no entity associated with " + modelClass + " in this database");
        return entity;
    }

    /**
     * Get the entities in this schema indexed by name.
     *
     * @return mapping from entity name to {@link Entity} as an unmodifiable map
     */
    public synchronized Map<String, Entity<?>> getEntityMap() {
        return Collections.unmodifiableMap(this.nameMap);
    }

    /**
     * Get the {@code "sidekarId"} implicit field.
     */
    public final IdField getIdField() {
        return this.idField;
    }

    /**
     * Get the {@code "sidekarEntity"} implicit field.
     */
    public final EntityField getEntityField() {
        return this.entityField;
    }

    /**
     * Get the {@code "sidekarAttached"} implicit field.
     */
    public final AttachedField getAttachedField() {
        return this.attachedField;
    }

// SchemaChangeSwitch handlers

    /**
     * Schema modification: Add a new entity. The entity will initially have zero fields.
     *
     * @throws SchemaModificationException if the entity name is illegal
     * @throws SchemaModificationException if an entity with the given name already exists
     * @throws SchemaModificationException if no entity with the given parent name exists
     * @throws SidekarException if there is an error in the underlying datastore
     */
    @Override
    @SuppressWarnings("unchecked")
    public synchronized void caseAddEntityChange(AddEntityChange change) {

        // Validate
        change.checkValidFor(this.schemaModel);

        // Perform checks
        this.checkValidEntityName(change.getEntityName());
        this.checkEntityNotExists(change.getEntityName());

        // Locate parent
        Entity<?> parentEntity = change.getParentName() != null ? this.nameMap.get(change.getParentName()) : null;

        // Update datastore
        this.getDatabase().schemaChange(change);

        // Update myself
        @SuppressWarnings("rawtypes")
        Entity<?> entity = new Entity(this, change.getEntityName(),
          change.getAbstractness(), parentEntity, Item.class, PlainItem.class);
        this.nameMap.put(change.getEntityName(), entity);
        this.recomputeDerivedInfo();
    }

    /**
     * Schema modification: Remove an entity.
     *
     * <p>
     * This automatically removes all indexes associated with the entity and all remaining instances of the entity.
     * </p>
     *
     * <p>
     * Requirements:
     * <ul>
     * </ul>
     *  <li>The entity must not be the parent of any other entities.</li>
     *  <li>There must be no reference fields in other entities that (directly) refer to this entity.</li>
     *  <li>There must be no actual references to any instances of this entity (other than by other instances of this entity).</li>
     * </ul>
     * </p>
     *
     * @throws SchemaModificationException if no entity with the given name exists
     * @throws SchemaModificationException if the entity is the parent of any other entity
     * @throws SchemaModificationException if a field in another entity refers to the entity
     * @throws SchemaModificationException if any instances of this entity are referred to
     */
    @Override
    public synchronized void caseRemoveEntityChange(RemoveEntityChange change) {

        // Perform checks
        final Entity<?> entity = this.nameMap.get(change.getEntityName());

        // Verify entity is a leaf
        for (Entity<?> child : this.nameMap.values()) {
            if (child.getParent() == entity) {
                throw new SchemaModificationException("can't remove " + entity
                  + " because it is a parent of " + child + ", which must be removed first");
            }
        }

        // Remove all indexes
        for (Index<?> index : entity.getIndexes())
            new RemoveIndexChange(change.getEntityName(), index.getName()).visit(this);

        // Remove all fields
        for (Field<?> field : entity.getEntityFields().values())
            new RemoveFieldChange(change.getEntityName(), field.getName()).visit(this);

        // Update datastore
        this.getDatabase().schemaChange(change);

        // Invalidate entity
        entity.invalidate();

        // Update myself
        this.nameMap.remove(entity.getName());
        this.recomputeDerivedInfo();
    }

    /**
     * Schema modification: Rename an entity.
     *
     * @throws SchemaModificationException if no such entity exists
     * @throws SchemaModificationException if the new name is illegal
     */
    @Override
    public synchronized void caseRenameEntityChange(RenameEntityChange change) {

        // Perform checks
        Entity<?> entity = this.nameMap.get(change.getEntityName());
        this.checkValidEntityName(change.getNewEntityName());
        this.checkEntityNotExists(change.getNewEntityName());

        // Update datastore
        this.getDatabase().schemaChange(change);

        // Update entity
        String oldName = entity.getName();
        entity.setName(change.getNewEntityName());

        // Update myself
        this.nameMap.remove(oldName);
        this.nameMap.put(entity.getName(), entity);
        this.recomputeDerivedInfo();
    }

    /**
     * Schema modification: Change an entity's abstractness.
     *
     * @throws SchemaModificationException if no such entity exists
     * @throws SchemaModificationException if any instances of this entity exist
     */
    @Override
    public synchronized void caseEntityAbstractnessChange(EntityAbstractnessChange change) {

        // Perform checks
        Entity<?> entity = this.nameMap.get(change.getEntityName());
        if (change.getAbstractness() == entity.isAbstract())
            return;

        // Update datastore
        this.getDatabase().schemaChange(change);

        // Update myself
        entity.setAbstract(change.getAbstractness());
        this.recomputeDerivedInfo();
    }

    /**
     * Schema modification: Add a field to an entity.
     *
     * @throws SchemaModificationException if no such entity exists
     * @throws SchemaModificationException if the entity or any sub-entity already contains a field with the given name
     * @throws SchemaModificationException if the field name is illegal
     */
    public synchronized void caseAddFieldChange(AddFieldChange change) {

        // Perform checks
        Entity<?> entity = this.nameMap.get(change.getEntityName());
        FieldModel fieldModel = change.getFieldModel();
        this.checkValidFieldName(fieldModel.getName());

        // Verify no name conflict with existing (sub)entity fields
        for (Entity<?> otherEntity : this.getSortedEntities()) {

            // See if otherEntity is a sub-entity of entity
            if (!entity.isAncestorOf(otherEntity))
                continue;

            // Look for a conflicting field
            if (otherEntity.getFieldMap().containsKey(fieldModel.getName())) {
                throw new SchemaModificationException(this.getDescription(otherEntity, entity)
                  + " already contains a field named \"" + fieldModel.getName() + "\"");
            }
        }

        // Create the new field
        final Field<?> field = fieldModel.visit(new FieldCreator(fieldModel.getName(), entity));

        // Update datastore
        this.getDatabase().schemaChange(change);

        // Update entity
        entity.addField(field);

        // Update myself
        this.recomputeDerivedInfo();
    }

    /**
     * Schema modification: Remove a field from an entity.
     * There must be no indexes defined on the field in the field's entity or any sub-entity.
     *
     * @throws SchemaModificationException if no such entity exists
     * @throws SchemaModificationException if no such field exists in the entity
     * @throws SchemaModificationException if field is a {@link SimpleField} and an index is
     *  defined on a field in the named entity or any sub-entity
     */
    @Override
    public synchronized void caseRemoveFieldChange(RemoveFieldChange change) {

        // Perform checks
        final Entity<?> entity = this.nameMap.get(change.getEntityName());

        // Find the field
        Field<?> field = entity.getEntityFields().get(change.getFieldName());
        if (field == null)
            throw new SchemaModificationException("no such field `" + change.getFieldName() + "' in " + entity);

        // Verify there are no indexes associated with the field
        Iterator<Entity<?>> i = this.getSortedEntities().iterator();
        while (i.next() != entity)
            ;
        while (i.hasNext()) {
            Entity<?> subEntity = i.next();
            for (Index<?> index : subEntity.getIndexes()) {
                for (String fieldName : index.getFieldNames()) {
                    if (fieldName.equals(field.getName())) {
                        throw new SchemaModificationException("can't remove field `" + fieldName + "' from " + entity
                          + " because it is indexed by index \"" + index.getName() + "\" of "
                          + this.getDescription(subEntity, entity));
                    }
                }
            }
        }

        // Update datastore
        this.getDatabase().schemaChange(change);

        // Invalidate field
        field.invalidate();

        // Update entity
        entity.removeField(field);

        // Update myself
        this.recomputeDerivedInfo();
    }

    /**
     * Schema modification: Add an index to an entity.
     *
     * @throws SchemaModificationException if no such entity exists
     * @throws SchemaModificationException if the entity or any sub-entity already contains an index with the given name
     * @throws SchemaModificationException if the index name is illegal
     */
    public synchronized void caseAddIndexChange(AddIndexChange change) {

        // Perform checks
        Entity<?> entity = this.nameMap.get(change.getEntityName());
        IndexModel indexModel = change.getIndexModel();
        this.checkValidIndexName(indexModel.getName());
        if (indexModel.getFieldNames().isEmpty())
            throw new SchemaModificationException("indexed field name list is empty");
        if (entity.getIndexMap().get(indexModel.getName()) != null) {
            throw new SchemaModificationException("an index named \""
              + indexModel.getName() + "\" on " + entity + " already exists");
        }

        // Check indexed fields
        Map<String, Field<?>> indexedEntityFields = entity.getFieldMap();
        HashSet<String> seenFieldNames = new HashSet<String>();
        for (String name : indexModel.getFieldNames()) {
            Field<?> indexedField = indexedEntityFields.get(name);
            if (indexedField == null)
                throw new SchemaModificationException("indexed field \"" + name + "\" does not exist in " + entity);
            if (!(indexedField instanceof SimpleField))
                throw new SchemaModificationException("indexed field \"" + name + "\" is not a simple field");
            if (seenFieldNames.contains(name))
                throw new SchemaModificationException("duplicate indexed field \"" + name + "\"");
            seenFieldNames.add(name);
        }

        // Update datastore
        this.getDatabase().schemaChange(change);

        // Update entity
        entity.addIndex(indexModel.getName(), indexModel.getFieldNames(), indexModel.isUnique());

        // Update myself
        this.recomputeDerivedInfo();
    }

    /**
     * Schema modification: remove an index from an entity.
     *
     * @throws SchemaModificationException if no such entity exists
     * @throws SchemaModificationException if no such index exists
     */
    public synchronized void caseRemoveIndexChange(RemoveIndexChange change) {

        // Perform checks
        Entity<?> entity = this.nameMap.get(change.getEntityName());
        Index<?> index = entity.getIndexMap().get(change.getIndexName());
        if (index == null)
            throw new SchemaModificationException("no index named \"" + change.getIndexName() + "\" exists on " + entity);

        // Update datastore
        this.getDatabase().schemaChange(change);

        // Invalidate index
        index.invalidate();

        // Update entity
        entity.removeIndex(change.getIndexName());

        // Update myself
        this.recomputeDerivedInfo();
    }

// Internal methods

    /**
     * Get class map.
     */
    Map<Class<?>, Entity<?>> getClassMap() {
        return this.classMap;
    }

    /**
     * Recompute all entity derived information. This must be invoked after any schema change.
     */
    private void recomputeDerivedInfo() {

        // Initialize entities
        for (Entity<?> entity : this.getSortedEntities())
            entity.initialize();

        // Regenerate class map
        this.classMap.clear();
        for (Entity<?> entity : this.nameMap.values()) {
            Class<?> userClass = entity.getUserClass();
            if (userClass != Item.class)
                this.classMap.put(userClass, entity);
        }
    }

    /**
     * Get all entities listed in order so that parents always appear before children.
     */
    private List<Entity<?>> getSortedEntities() {
        TopologicalSorter.EdgeLister<Entity<?>> edgeLister = new TopologicalSorter.EdgeLister<Entity<?>>() {
            @Override
            public Set<Entity<?>> getOutEdges(Entity<?> node) {
                return node.getParent() != null ?
                  new HashSet<Entity<?>>(Collections.singleton(node.getParent())) : Collections.<Entity<?>>emptySet();
            }
        };
        return new TopologicalSorter<Entity<?>>(this.nameMap.values(), edgeLister).sortEdgesReversed();
    }

// Field creator

    class FieldCreator implements FieldModelSwitch<Field<?>> {

        private final String fieldName;
        private final Entity<?> entity;

        public FieldCreator() {
            this(null, null);
        }

        public FieldCreator(String fieldName, Entity<?> entity) {
            this.fieldName = fieldName;
            this.entity = entity;
        }

        @Override
        public ReferenceField<?> caseReferenceFieldModel(ReferenceFieldModel fieldModel) {
            return createReferenceField(this.fieldName, this.entity,
              Schema.this.nameMap.get(fieldModel.getReferentModel().getName()));
        }

        @Override
        public SetField<?> caseSetFieldModel(SetFieldModel fieldModel) {
            return this.createSetField(this.createSimpleField(fieldModel.getElementModel()));
        }

        @Override
        public ListField<?> caseListFieldModel(ListFieldModel fieldModel) {
            return this.createListField(createSimpleField(fieldModel.getElementModel()));
        }

        @Override
        public MapField<?, ?> caseMapFieldModel(MapFieldModel fieldModel) {
            return this.createMapField(this.createSimpleField(fieldModel.getKeyModel()),
              this.createSimpleField(fieldModel.getValueModel()));
        }

        @Override
        public ArrayField<?> caseArrayFieldModel(ArrayFieldModel fieldModel) {
            return this.createArrayField(this.createAtomicField(fieldModel.getElementModel()));
        }

        @Override
        public StringField caseStringFieldModel(StringFieldModel fieldModel) {
            return new StringField(this.fieldName, this.entity);
        }

        @Override
        public DateField caseDateFieldModel(DateFieldModel fieldModel) {
            return new DateField(this.fieldName, this.entity);
        }

        @Override
        public BigIntegerField caseBigIntegerFieldModel(BigIntegerFieldModel fieldModel) {
            return new BigIntegerField(this.fieldName, this.entity);
        }

        @Override
        public BooleanField caseBooleanFieldModel(BooleanFieldModel fieldModel) {
            return new BooleanField(this.fieldName, this.entity);
        }

        @Override
        public ByteField caseByteFieldModel(ByteFieldModel fieldModel) {
            return new ByteField(this.fieldName, this.entity);
        }

        @Override
        public CharacterField caseCharacterFieldModel(CharacterFieldModel fieldModel) {
            return new CharacterField(this.fieldName, this.entity);
        }

        @Override
        public ShortField caseShortFieldModel(ShortFieldModel fieldModel) {
            return new ShortField(this.fieldName, this.entity);
        }

        @Override
        public IntegerField caseIntegerFieldModel(IntegerFieldModel fieldModel) {
            return new IntegerField(this.fieldName, this.entity);
        }

        @Override
        public FloatField caseFloatFieldModel(FloatFieldModel fieldModel) {
            return new FloatField(this.fieldName, this.entity);
        }

        @Override
        public DoubleField caseDoubleFieldModel(DoubleFieldModel fieldModel) {
            return new DoubleField(this.fieldName, this.entity);
        }

        @Override
        public LongField caseLongFieldModel(LongFieldModel fieldModel) {
            return new LongField(this.fieldName, this.entity);
        }

        private AtomicField<?> createAtomicField(AtomicFieldModel fieldModel) {
            return (AtomicField<?>)fieldModel.visit(Schema.this.new FieldCreator());
        }

        private SimpleField<?> createSimpleField(SimpleFieldModel fieldModel) {
            return (SimpleField<?>)fieldModel.visit(Schema.this.new FieldCreator());
        }

        // This method exists to bind generic type(s)
        private <E> ReferenceField<E> createReferenceField(String fieldName, Entity<?> entity, Entity<E> referent) {
            return new ReferenceField<E>(this.fieldName, this.entity, referent);
        }

        // This method exists to bind generic type(s)
        private <E> SetField<E> createSetField(SimpleField<E> elemField) {
            return new SetField<E>(this.fieldName, this.entity, elemField);
        }

        // This method exists to bind generic type(s)
        private <E> ListField<E> createListField(SimpleField<E> elemField) {
            return new ListField<E>(this.fieldName, this.entity, elemField);
        }

        // This method exists to bind generic type(s)
        private <K, V> MapField<K, V> createMapField(SimpleField<K> keyField, SimpleField<V> valueField) {
            return new MapField<K, V>(this.fieldName, this.entity, keyField, valueField);
        }

        // This method exists to bind generic type(s)
        private <E> ArrayField<E> createArrayField(AtomicField<E> elemField) {
            return new ArrayField<E>(this.fieldName, this.entity, elemField);
        }
    }
}

